//---------------------------------------------------------------------
// Author: Keith Hill
//
// Description: Class to implement the Test-Xml cmdlet which checks
//              for well-formed by default and optionally validates 
//              against supplied schema.
//
// Creation Date: Sept 27, 2006
//---------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Management.Automation;
using System.Security;
using System.Text;
using System.Xml;
using System.Xml.Schema;

namespace Pscx.Commands.Xml
{
    [Cmdlet(VerbsDiagnostic.Test, "Xml", DefaultParameterSetName = "Path"),
     Description("Tests for well formedness and optionally validates against XML Schema."),
     DetailedDescription("Tests for well formedness and optionally validates against XML Schema.  It doesn't handle specifying the targetNamespace.  To see validation error messages, specify the -Verbose flag."),
     RelatedLinks("Convert-Xml"), RelatedLinks("Format-Xml")]
    public class TestXmlCommand : PSCmdlet
    {
        private PSObject _inputObject;
        private string[] _paths;
        private bool _literalPathUsed;
        private string[] _schemaPaths;
        private XmlSchemaSet _schemaSet;
        private bool _validate;
        private bool _validationErrorOccurred;

        [Parameter(ParameterSetName = "Path",
                   Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true,
                   HelpMessage = "Specifies the path to the file to process. Wildcard syntax is allowed."),
         AcceptsWildcards(true)]
        public string[] Path
        {
            get { return _paths; }
            set { _paths = value; }
        }

        [Alias(new string[] { "PSPath" }),
         Parameter(ParameterSetName = "LiteralPath",
                   Position = 0, Mandatory = true, ValueFromPipeline = false, ValueFromPipelineByPropertyName = true,
                   HelpMessage = "Specifies a path to the item. The value of -LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell Windows PowerShell not to interpret any characters as escape sequences."),
         AcceptsWildcards(false)]
        public string[] LiteralPath
        {
            get { return _paths; }
            set
            {
                _paths = value;
                _literalPathUsed = true;
            }
        }

        [AllowNull,
         AllowEmptyString,
         Parameter(ParameterSetName = "Object",
                   Mandatory = true, ValueFromPipeline = true,
                   HelpMessage = "Accepts an object as input to the cmdlet. Enter a variable that contains the objects or type a command or expression that gets the objects.")]
        public PSObject InputObject
        {
            get { return _inputObject; }
            set { _inputObject = value; }
        }

        [Parameter(HelpMessage = "Array of paths to the required schema files to perform schema-based validation."),
         AcceptsWildcards(false)]
        public string[] SchemaPaths
        {
            get { return _schemaPaths; }
            set { _schemaPaths = value; }
        }

        [Parameter(HelpMessage = "Forces schema validation of the XML against inline schema.")]
        public SwitchParameter Validate
        {
            get { return (SwitchParameter)_validate; }
            set { _validate = (bool)value; }
        }

        protected override void BeginProcessing()
        {
            _schemaSet = new XmlSchemaSet();

            // If not schema paths specified then we're done.
            if (_schemaPaths == null) return;

            List<string> resolvedSchemaPaths = Utils.ResolveFilePaths(this, _schemaPaths);
            if (resolvedSchemaPaths.Count == 0) return;

            foreach (string _schemaPath in resolvedSchemaPaths)
            {
                using (FileStream stream = new FileStream(_schemaPath, FileMode.Open, FileAccess.Read))
                {
                    XmlSchema xmlSchema = XmlSchema.Read(stream, SchemaValidationHandler);
                    _schemaSet.Add(xmlSchema);
                }
            }

            // If the user specified a schema they want validation
            _validate = true;
        }

        private void SchemaValidationHandler(object sender, ValidationEventArgs e)
        {
            WriteError(new ErrorRecord(e.Exception, "SchemaInvalid", ErrorCategory.InvalidData, e.Message));
        }

        protected override void ProcessRecord()
        {
            List<string> pathList = null;
            string xmlString = null;

            if (_paths != null)
            {
                pathList = Utils.ResolveFilePaths(this, _paths, _literalPathUsed);
            }
            else if (_inputObject != null)
            {
                if ((_inputObject.BaseObject as FileInfo) != null)
                {
                    FileInfo fileInfo = _inputObject.BaseObject as FileInfo;
                    pathList = new List<string>();
                    pathList.Add(fileInfo.FullName);
                }
                else if ((_inputObject.BaseObject as XmlDocument) != null)
                {
                    XmlDocument xmlDoc = _inputObject.BaseObject as XmlDocument;
                    xmlString = xmlDoc.InnerXml;
                }
                else if (_inputObject.BaseObject is string)
                {
                    xmlString = (string)_inputObject.BaseObject;
                }
                else
                {
                    string msg = String.Format(Resources.Shared.XmlBadPipeObj_Fmt1, _inputObject.BaseObject.GetType().Name);
                    ArgumentException ex = new ArgumentException(msg);
                    WriteError(new ErrorRecord(ex, "InvalidPipelineObjectType", ErrorCategory.InvalidData, _inputObject));
                }
            }

            if (pathList != null)
            {
                foreach (string path in pathList)
                {
                    WriteDebug("Processing file " + path);
                    TestXmlFile(path);
                }
            }
            else if (xmlString != null)
            {
                WriteDebug("Processing input string " + xmlString.Substring(0, Math.Min(40, xmlString.Length)));
                TestXmlString(xmlString);
            }
            else
            {
                WriteDebug("Ignoring null input");
            }
        }

        private void TestXmlFile(string path)
        {
            ProviderInfo providerInfo;
            Collection<string> rpaths = GetResolvedProviderPathFromPSPath(path, out providerInfo);
            foreach (string rpath in rpaths)
            {
                try
                {
                    FileStream fileStream = new FileStream(rpath, FileMode.Open, FileAccess.Read);
                    TestXml(new StreamReader(fileStream));
                }
                catch (FileNotFoundException ex)
                {
                    WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.ObjectNotFound, rpath));
                }
                catch (SecurityException ex)
                {
                    WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.SecurityError, rpath));
                }
                catch (UnauthorizedAccessException ex)
                {
                    WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.SecurityError, rpath));
                }
                catch (XmlException ex)
                {
                    WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.InvalidOperation, rpath));
                }
                catch (Exception ex)
                {
                    WriteError(new ErrorRecord(ex, "FileError", ErrorCategory.NotSpecified, rpath));
                }
            }
        }

        private void TestXmlString(string xmlText)
        {
            try
            {
                StringReader stringReader = new StringReader(xmlText);
                TestXml(stringReader);
            }
            catch (XmlException ex)
            {
                WriteError(new ErrorRecord(ex, "InvalidInputError", ErrorCategory.InvalidOperation, xmlText));
            }
            catch (Exception ex)
            {
                WriteError(new ErrorRecord(ex, "InvalidInputError", ErrorCategory.NotSpecified, xmlText));
            }
        }

        private void TestXml(TextReader textReader)
        {
            _validationErrorOccurred = false;

            XmlReader xmlReader = null;
            try
            {
                XmlReaderSettings readerSettings = new XmlReaderSettings();
                readerSettings.CloseInput = true;
                readerSettings.CheckCharacters = true;

                if (_validate)
                {
                    readerSettings.ValidationType = ValidationType.Schema;
                    readerSettings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
                    if (_schemaSet != null)
                    {
                        readerSettings.Schemas.Add(_schemaSet);
                    }
                    readerSettings.ValidationEventHandler += SchemaValidationEventHandler;
                }
                xmlReader = XmlReader.Create(textReader, readerSettings);
                while (xmlReader.Read());

                WriteObject(!_validationErrorOccurred);
            }
            catch (XmlException ex)
            {
                WriteVerbose(ex.Message);
                WriteObject(false);
            }
            finally
            { 
                if (xmlReader != null) xmlReader.Close();
            }
        }

        void SchemaValidationEventHandler(object sender, System.Xml.Schema.ValidationEventArgs e)
        {
            if (e.Severity == XmlSeverityType.Error)
            {
                _validationErrorOccurred = true;
            }

            WriteVerbose(e.Severity.ToString() + ": " + e.Message);
        }
    }
}